{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"images/strathsdr_banner.png\" align=\"left\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# RFSoC QPSK Transceiver\n",
    "\n",
    "----\n",
    "\n",
    "<div class=\"alert alert-box alert-info\">\n",
    "Please use Jupyter Labs http://board_ip_address/lab for this notebook.\n",
    "</div>\n",
    "\n",
    "This design is a full QPSK transceiver, which transmits and receives randomly-generated pulse-shaped symbols with full carrier and timing synchronisation. PYNQ is used to visualise the data at both the DAC and ADC side of the RFSoC data converters, as well as visualising various DSP stages throughout the transmit and receive signal path.\n",
    "\n",
    " \n",
    "## Contents    \n",
    "* [Introduction](#introduction)\n",
    "    * [Hardware Setup](#hardware-setup)\n",
    "    * [Software Setup](#software-setup)\n",
    "* [RFSoC QPSK Transceiver](#RFSoC-QPSK-Transceiver)\n",
    "    * [Inspecting the transmit path](#Inspecting-the-transmit-path)\n",
    "    * [Inspecting the receive path](#Inspecting-the-receive-path)\n",
    "    * [Reconfigure the RF Data Path](#Reconfigure-the-RF-Data-Path)\n",
    "* [Conclusion](#conclusion)\n",
    "    \n",
    "## References\n",
    "* [Xilinx, Inc, \"USP RF Data Converter: LogiCORE IP Product Guide\", PG269, v2.3, June 2020](https://www.xilinx.com/support/documentation/ip_documentation/usp_rf_data_converter/v2_3/pg269-rf-data-converter.pdf)\n",
    "\n",
    "## Revision History\n",
    "* **v1.0** | 02/11/2020 | RFSoC QPSK demonstrator\n",
    "* **v1.1** | 23/02/2021 | Reformatted notebook"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "----"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Introduction <a class=\"anchor\" id=\"introduction\"></a>\n",
    "The RFSoC2x2 development board can be configured as a simple QPSK transceiver. The RFSoC QPSK demonstrator uses the RFSoC's RF Data Converters (RF DCs) to transmit and receive QPSK modulated waveforms. There are setup steps for hardware and software that you must follow.\n",
    "\n",
    "### Hardware Setup <a class=\"anchor\" id=\"hardware-setup\"></a>\n",
    "Your RFSoC2x2 development board can be configured to host one QPSK transceiver channel. To setup your board for this demonstration, you can connect a channel in loopback as shown in [Figure 1](#fig-1).\n",
    "\n",
    "The default loopback configuration is connected as follows:\n",
    "* Channel 0: DAC2 to ADC2\n",
    "\n",
    "Use the image below for further guidance.\n",
    "\n",
    "<a class=\"anchor\" id=\"fig-1\"></a>\n",
    "<figure>\n",
    "<img src='images/rfsoc2x2_setup.jpg' height='50%' width='50%'/>\n",
    "    <figcaption><b>Figure 1: RFSoC2x2 development board setup in loopback mode.</b></figcaption>\n",
    "</figure>\n",
    "\n",
    "**Do not** attach an antenna to any SMA interfaces labelled DAC.\n",
    "\n",
    "<div class=\"alert alert-box alert-danger\">\n",
    "<b>Caution:</b>\n",
    "    In this demonstration, we generate tones using the RFSoC development board. Your device should be setup in loopback mode. You should understand that the RFSoC platform can also transmit RF signals wirelessly. Remember that unlicensed wireless transmission of RF signals may be illegal in your geographical location. Radio signals may also interfere with nearby devices, such as pacemakers and emergency radio equipment. Note that it is also illegal to intercept and decode particular RF signals. If you are unsure, please seek professional support.\n",
    "</div>\n",
    "\n",
    "### Software Setup <a class=\"anchor\" id=\"software-setup\"></a>\n",
    "Start by including the `xrfdc` drivers so we can configure the RF data converters, `ipywidgets` to make interactive controls, `numpy` for numerical analysis, and `rfsoc_qpsk` for the QPSK design."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import xrfdc\n",
    "import ipywidgets as ipw\n",
    "import numpy as np\n",
    "\n",
    "from rfsoc_qpsk.qpsk_overlay import QpskOverlay"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can now initialise the overlay by downloading the bitstream and executing the drivers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol = QpskOverlay()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For a quick reference of all the things you can do with the QPSK overlay, ask the Python interpreter!\n",
    "Pop open a new console (right click here and select \"_New Console for Notebook_\") and type `ol.plot_group?` to query a method of our new overlay. Tab completion works for discovery too.\n",
    "\n",
    "----"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## RFSoC QPSK Transceiver <a class=\"anchor\" id=\"RFSoC-QPSK-Transceiver\"></a>\n",
    "We will now explore three interesting components of the QPSK demonstrator. Initially, the transmit path will be inspected and then the same inspection will also be carried out on the receive path. Finally, we will explore the control capabilities of our design and determine how these properties affect the transmit and receive signals.\n",
    "\n",
    "### Inspecting the transmit path <a class=\"anchor\" id=\"Inspecting-the-transmit-path\"></a>\n",
    "\n",
    "There are 3 main steps in the QPSK transmit IP signal path:\n",
    "\n",
    "1. Random symbol generation\n",
    "2. Pulse shaping\n",
    "3. Interpolation\n",
    "  \n",
    "This design \"taps off\" this path after the first two stages so we can inspect the signals in Jupyter Lab.\n",
    "The RF data converter can be reconfigured from Python too - we'll look at that [later](#Reconfigure-the-RF-Data-Path).\n",
    "\n",
    "![](./images/block_diagram/QPSK_system_block_diagrams_Tx_only.svg)\n",
    "\n",
    "First we plot our raw QPSK symbols in the time domain."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.plot_group(\n",
    "  'tx_symbol',            # Plot group's ID\n",
    "  ['time-binary'],        # List of plot types chosen from:\n",
    "                          #   ['time','time-binary','frequency','constellation']\n",
    "  ol.qpsk_tx.get_symbols, # Function to grab a buffer of samples\n",
    "  500                     # Sample frequency (Hz)\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can stream new samples into this plot using the play/stop buttons. By default the samples are stored in a rolling buffer, so we can keep this running for a while without worrying too much about total memory usage. As you continue to work through this notebook though, you should stop any previous plot streams to keep your browser happy.\n",
    "\n",
    "For the pulse shaped signal, let's have a look at the frequency domain too. This FFT is accelerated in the PL so we pass in an extra argument, `get_freq_data`, telling the plotting library how to grab the accelerated FFT data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.plot_group('tx_shaped', ['time', 'frequency'], ol.qpsk_tx.get_shaped_time, 4000,\n",
    "              get_freq_data=ol.qpsk_tx.get_shaped_fft)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Inspecting the receive path <a class=\"anchor\" id=\"Inspecting-the-receive-path\"></a>\n",
    "\n",
    "The receive side is nearly the inverse of the transmit path (there's just some extra work for properly synchronising).\n",
    "\n",
    "Again, there are taps off from a few places in the signal path:\n",
    "\n",
    "1. After decimation\n",
    "2. After coarse synchronisation\n",
    "3. After root-raised-cosine filtering\n",
    "4. and the data output\n",
    "\n",
    "![](./images/block_diagram/QPSK_system_block_diagrams_Rx_only.svg)\n",
    "\n",
    "Because there are a few different intermediate stages, let's reuse the same cells to plot any of them on-demand.\n",
    "\n",
    "First we describe how to generate plots for each of the intermediate taps."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rx_domains = ['time', 'frequency', 'constellation']\n",
    "\n",
    "plot_rx_decimated   = lambda : ol.plot_group(\n",
    "    'rx_decimated',   rx_domains, ol.qpsk_rx.get_decimated,     4000\n",
    ")\n",
    "\n",
    "plot_rx_coarse_sync = lambda : ol.plot_group(\n",
    "    'rx_coarse_sync', rx_domains, ol.qpsk_rx.get_coarse_synced, 4000\n",
    ")\n",
    "\n",
    "plot_rx_rrced       = lambda : ol.plot_group(\n",
    "    'rx_rrced',       rx_domains, ol.qpsk_rx.get_rrced,         16000\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we can just execute the function whichever tap you want. For example, let's look at the tap after decimation below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_rx_decimated()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And for the final plot, let's look at the synchronised output data. To recover the bits we need to take our sampled, synchronised signal (seen in the constellation plot below) and decide which quadrant each symbol has fallen into."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def classify_bits(frame):\n",
    "    bit_quantise    = lambda b: 1 if b>0 else 0\n",
    "    symbol_quantise = lambda i, q: bit_quantise(i) + 1j*bit_quantise(q)\n",
    "    return np.fromiter(\n",
    "        map(symbol_quantise, np.real(frame), np.imag(frame)),\n",
    "        dtype=complex\n",
    "    )\n",
    "\n",
    "ol.plot_group(\n",
    "    'rx_data',\n",
    "    ['constellation', 'time-binary'],\n",
    "    lambda : classify_bits(ol.qpsk_rx.get_data()),\n",
    "    500,\n",
    "    get_const_data=ol.qpsk_rx.get_data\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now is a good time to note that Jupyter Lab can manage multiple windows. Next we'll be playing with the RF settings, so you may want to make a new window for the constellation plot and leave it streaming. Make a new window for the plot by right clicking the plot and selecting \"_Create New View for Output_\". Feel free to snap this new window to the side by clicking the window's title (\"Output View\") and dragging it to the side of the web page. Now we can play with RF settings further down the notebook while still getting instant feedback about our received signal — pretty neat!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Reconfigure the RF Data Path <a class=\"anchor\" id=\"Reconfigure-the-RF-Data-Path\"></a>\n",
    "\n",
    "#### Transmit Power\n",
    "\n",
    "The QPSK bitstream includes a digital attenuator on the transmit path. We can configure this via a memory-mapped register.\n",
    "\n",
    "Let's use this as an example of interactive reconfiguration because the effects are quite clear in the constellation diagram. Try reducing the output power by setting a gain between 0 (off) and 1 (full scale)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.qpsk_tx.set_gain(0.6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The constellation plot should shrink in a little towards the origin. Let's return to full power now."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ol.qpsk_tx.set_gain(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can use some `ipywidgets` to make a more natural interface to control the gain too. Let's expose this as a slider with a callback to the `set_gain` function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pow_slider = ipw.SelectionSlider(\n",
    "    options=[0.1, 0.3, 0.6, 1],\n",
    "    value=1,\n",
    "    description='',\n",
    ")\n",
    "\n",
    "accordion = ipw.Accordion(children=[pow_slider])\n",
    "accordion.set_title(0, 'Transmitter power')\n",
    "display(accordion)\n",
    "\n",
    "def unwrap_slider_val(callback):\n",
    "    return lambda slider_val : callback(slider_val['new'])\n",
    "\n",
    "pow_slider.observe(unwrap_slider_val(ol.qpsk_tx.set_gain), names='value')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Transmit and Receive Mixer Settings\n",
    "\n",
    "So far the RF Data Converter settings have been controlled by `QpskOverlay` but we can also reconfigure these on the fly in python with the `xrfdc` driver.\n",
    "\n",
    "First of all, consider the DAC block used for the transmit side.\n",
    "\n",
    "![](./images/block_diagram/RF_DAC.svg)\n",
    "\n",
    "There's a lot of scope for reconfiguration here — see the [IP product guide](https://www.xilinx.com/support/documentation/ip_documentation/usp_rf_data_converter/v2_1/pg269-rf-data-converter.pdf) or type `ol.dac_block?` for more details.\n",
    "\n",
    "As an example, let's play with the mixer settings. Try changing the DAC's mixer frequency from the deafult 1000 MHz to 900 MHz."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def update_nco(rf_block, nco_freq):\n",
    "    mixer_cfg = rf_block.MixerSettings\n",
    "    mixer_cfg['Freq'] = nco_freq\n",
    "    rf_block.MixerSettings = mixer_cfg\n",
    "    rf_block.UpdateEvent(xrfdc.EVENT_MIXER)\n",
    "\n",
    "update_nco(ol.dac_block, 900)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The received signal should disappear until we configure the receiver's ADC to match the new carrier frequency. Set the new carrier frequency for the ADC side mixer below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "update_nco(ol.adc_block, 900)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Again, we can use `ipywidgets` to make an interactive interface for these settings. Below we setup an RX and a TX slider and a TX slider that are linked together so we can scrub along the spectrum keeping both sides in near lock-step. If you've got any analog RF filters to hand, try them out with different mixer settings!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def new_nco_slider(title):\n",
    "    return ipw.FloatSlider(\n",
    "        value=1000,\n",
    "        min=620,\n",
    "        max=1220,\n",
    "        step=20,\n",
    "        description=title,\n",
    "        disabled=False,\n",
    "        continuous_update=False,\n",
    "        orientation='horizontal',\n",
    "        readout=True,\n",
    "        readout_format='.1f',\n",
    "    )\n",
    "\n",
    "tx_nco_slider = new_nco_slider('TX (MHz)')\n",
    "rx_nco_slider = new_nco_slider('RX (MHz)')\n",
    "\n",
    "accordion = ipw.Accordion(children=[ipw.VBox([tx_nco_slider, rx_nco_slider])])\n",
    "accordion.set_title(0, 'Carrier frequency')\n",
    "display(accordion)\n",
    "\n",
    "ipw.link((rx_nco_slider, 'value'), (tx_nco_slider, 'value'))\n",
    "tx_nco_slider.observe(\n",
    "    unwrap_slider_val(lambda v: update_nco(ol.dac_block, v)),\n",
    "    names='value'\n",
    ")\n",
    "rx_nco_slider.observe(\n",
    "    unwrap_slider_val(lambda v: update_nco(ol.adc_block, v)),\n",
    "    names='value'\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Conclusion <a class=\"anchor\" id=\"conclusion\"></a>\n",
    "\n",
    "We've now lead you through how we can interact with the RF data converters from PYNQ, using a QPSK transmit/receive loopback system as an example. More exhaustively, we've shown:\n",
    "\n",
    "  * Use of the programmable logic in the context of a real RF application\n",
    "  * Performing on-board introspection of an RF design:\n",
    "      * Leveraging existing plotting libraries from the Python ecosystem\n",
    "  * Interacting with a QPSK hardware design\n",
    "      * Configuring the signal path, using transmit power as an example\n",
    "      * Configuring the RF data converter, using TX/RX mixer frequencies as an example"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.2"
  },
  "widgets": {
   "application/vnd.jupyter.widget-state+json": {
    "state": {},
    "version_major": 2,
    "version_minor": 0
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
